/*
 * Application Blocker for a social network
 * written by Fabian Mueller (info@crashsource.de)
 *
 * If you've any ideas for improvement create a new issue here:
 * http://code.google.com/p/fbapplicationblocker/issues/list
 */


/*
 * -- Storage class --
 * This class allowes us to save data in the HTML5-storage.
 */
function Storage() {
	// nothing to do here, but necessary for javascript
}

Storage.set = function(key, value) {
	try {
		delete localStorage[key];
		localStorage[key] = value;
	}catch(e) {
		console.log("Error inside Storage.set()");
		console.log(e);
	}
}

Storage.get = function(key) {
	var value;
		try {
		value = localStorage[key];
	}catch(e) {
		console.log("Error inside Storage.get() for key: " + key);
		console.log(e);
		value = null;
	}
	return value;
}

Storage.createDefaultValues = function() {
	if (Storage.get('highlightItems') == null) {
		Storage.set('highlightItems', 'true');
	}
	if (Storage.get('blockAppEntries') == null) {
		Storage.set('blockAppEntries', 'true');
	}
	if (Storage.get('showFoundBox') == null) {
		Storage.set('showFoundBox', 'true');
	}
	if (Storage.get('highlightColor') == null) {
		Storage.set('highlightColor', '#d7dbe5');
	}
	if (Storage.get('unblockPostsWithComments') == null) {
		Storage.set('unblockPostsWithComments', 'false');
	}
	if (Storage.get('whitelist') == null) {
		Whitelist.reset();
	}
}

/*
 * -- Whitelist class --
 * This class allowes us to load, save and manipulate the
 * users whitelist.
 */
function Whitelist() {
	this.list = [];
}

Whitelist.storageId = "whitelist";
Whitelist.sepApplication = "<!sepApplication>";
Whitelist.sepWhitelist = "<!sepWhitelist>";

Whitelist.prototype.load = function() {
	if (Storage.get(Whitelist.storageId) == null) {
		Storage.set(Whitelist.storageId, this.serialize());
	}
	else {
		this.list = this.deserialize(Storage.get(Whitelist.storageId));
	}
}

Whitelist.prototype.isWhitelisted = function(appId) {
	for (var i=0; i<this.list.length; i++) {
		if (this.list[i][0] == appId) {
			return true;
		}
	}
	return false;
}

Whitelist.prototype.serialize = function(whitelist) {
	try {
		if (whitelist.length == 0) return "";
	}
	catch (err) {
		console.error("given whitelist is not valid: " + err.message);
	}
	var res = "";
	for (var i=0; i<whitelist.length; i++) {
		res = res + whitelist[i][0] + Whitelist.sepWhitelist + whitelist[i][1] + Whitelist.sepApplication;
	}
	return res;
}

Whitelist.prototype.deserialize = function(whitelist) {
	var res = [];	
	if (whitelist == "") return res;

	var apps = whitelist.split(Whitelist.sepApplication);
	for (var i=0; i<apps.length-1; i++) {
		var splitted = apps[i].split(Whitelist.sepWhitelist);
		res.push(new Array(splitted[0],splitted[1]));
	}
	return res;
}

Whitelist.prototype.add = function(appId, appName) {
	if (this.isWhitelisted(appId)) return true;
	try {
		this.list.push(Array(appId, appName));
		Storage.set(Whitelist.storageId, this.serialize(this.list));
	}
	catch(err) {
		console.error(err.message);
		return false;
	}
	return true;
}

Whitelist.prototype.remove = function(appId) {
	for(var i=0; i<this.list.length; i++) {
		if (this.list[i][0] == appId) {
			this.list.splice(i,1);
			Storage.set(Whitelist.storageId, this.serialize(this.list));
			return true;
		}
	}
	return false;
}

Whitelist.prototype.getWhitelist = function() {
	return this.list.slice(0);
}

Whitelist.reset = function() {
	var whitelistTemp = new Whitelist();
	whitelistTemp.add(6628568379, "Facebook for iPhone");
	whitelistTemp.add(350685531728, "Facebook");
	whitelistTemp.add(6212371378, "TweetDeck");
	whitelistTemp.add(41158896424, "HTC Sense");
	whitelistTemp.add(72687635881, "Samsung Mobile");
	whitelistTemp.add(43769862066, "SonyEricsson");
}

/*
 * -- Menu class --
 */
function Menu() {
	// nothing to do here, but this is necessary for javascript
}
Menu.index = { 'filter' : 1, 'unblockPostsWithComments': 2, 'highlight' : 3, 'foundBox' : 4};

Menu.create = function() {
	// create target-div to store our extension settings
	var $jewelCase = $('#jewelCase');
	var $appBlockWrapper = $('<div/>', {
		'id' : 'appBlockWrapper',
		'class': 'jewel'
		});
	$appBlockWrapper.appendTo($jewelCase);

	// make more space for our target-div
	// -> calculate the new values
	var searchLeftPx = $('#headNav').css('margin-left');
	searchLeftPx = searchLeftPx.substring(0,searchLeftPx.length-2);
	$('#headNav').css('margin-left', (parseInt(searchLeftPx) + 20) + 'px');
	var jewelCaseLeftPx = $jewelCase.css('width');
	jewelCaseLeftPx = jewelCaseLeftPx.substring(0,jewelCaseLeftPx.length-2);
	$jewelCase.css('width', (parseInt(jewelCaseLeftPx) + 20) + 'px');

	// make our target-div feel like a original one
	var $wrapperA = $('#notificationsWrapper > a').clone();
	$wrapperA.attr({
		id : 'jewelAppBlock',
		title: chrome.i18n.getMessage("app_name"),
		href: 'http://crashsource.de/chrome-extension-application-blocker-social-network/',
		accesskey: '6'
		});
	$wrapperA.appendTo($appBlockWrapper);

	var $jewelBoxAppBlock = $('<div/>', {
		'id' : 'jewelBoxAppBlocker',
		'class' : 'jewelBox jewelHighlightItem uiToggleFlyout'
		});

	$('<div class="uiHeader uiHeaderBottomBorder uiHeaderSection jewelHeader"><div class="clearfix uiHeaderTop"><div><h3 class="uiHeaderTitle">'+chrome.i18n.getMessage('menu_headline')+'</h3></div></div></div>')
		.appendTo($jewelBoxAppBlock);

	var $settingsList = $('<ul/>', {
		'id' : 'jewelAppBlockSettings'
		});

	// add our settings
	$settingsList.append($('<li title="'+chrome.i18n.getMessage('menu_counter_description')+'">'+chrome.i18n.getMessage('menu_counter_label')+' <span id="appBlockCounter"></span></li>'));
	var $filterOption = $('<li title="'+chrome.i18n.getMessage('menu_filter_description')+'">'+chrome.i18n.getMessage('menu_filter_label')+' <span><input type="checkbox" ' + ((Storage.get('blockAppEntries') == 'true') ? 'checked="checked"' : '') + ' /></span></li>').appendTo($settingsList);
	var $unblockPostsWithCommentsOption = $('<li title="'+chrome.i18n.getMessage('menu_filter_comment_description')+'">'+chrome.i18n.getMessage('menu_filter_comment_label')+' <span><input type="checkbox" ' + ((Storage.get('unblockPostsWithComments') == 'true') ? 'checked="checked"' : '') + ' /></span></li>').appendTo($settingsList);

var $highlightOption = $('<li title="'+chrome.i18n.getMessage('menu_highlight_description')+'">'+chrome.i18n.getMessage('menu_highlight_label')+' <span><input type="checkbox" ' + ((Storage.get('highlightItems') == 'true') ? 'checked="checked"' : '') + ' /></span></li>').appendTo($settingsList);
	var $foundBoxOption = $('<li title="'+chrome.i18n.getMessage('menu_foundbox_description')+'">'+chrome.i18n.getMessage('menu_foundbox_label')+' <span><input type="checkbox" ' + ((Storage.get('showFoundBox') == 'true') ? 'checked="checked"' : '') + ' /></span></li>').appendTo($settingsList);
	
	$settingsList.append($('<li title="'+chrome.i18n.getMessage('menu_highlight_color_description')+'">'+chrome.i18n.getMessage('menu_highlight_color_label')+' <span><ul id="appBlockColors"></ul></span></li>'));
	var $whitelistOption = $('<li title="'+chrome.i18n.getMessage('menu_whitelist_description')+'">'+chrome.i18n.getMessage('menu_whitelist_label')+' <span><a href="#" class="hot-link" title="'+chrome.i18n.getMessage('menu_reset_description')+'">'+chrome.i18n.getMessage('reset')+'</a></span><div id="appBlockWhitelist" style="display: none;"><ul></ul></div></li>').appendTo($settingsList);
	
	$settingsList.appendTo($jewelBoxAppBlock);

	// add the footer
	$('<div class="jewelFooter"><a class="seeMore" href="http://crashsource.de/chrome-extension-application-blocker-social-network/">'+chrome.i18n.getMessage('menu_learn_more')+'</a></div>')
		.appendTo($jewelBoxAppBlock);

	$jewelBoxAppBlock.appendTo($appBlockWrapper);

	// add colors for highlighting
	var highlightColorList = ["#d7dbe5", "#d4d4d4", "#efc091", "#c1efbf", "#d3b5ff"];
	var $colors = $('#appBlockColors');
	for (var i=0; i<highlightColorList.length; i++) {
		$colors.append($('<li style="background-color: ' + highlightColorList[i] + '"></li>'));
	}
	$('#appBlockColors > li').bind('click', function() {
		//console.log('color changed to: ' + $(this).css('background-color'));
		Storage.set('highlightColor', $(this).css('background-color'));
		manipulateFeed(getFeed());
	});

	// cover hover effects to simulate the original behavior. we want to be cool, too.
	$('#jewelAppBlockSettings > li').hover(
		function() {
			$(this).addClass('selected');
		}, function(){
			$(this).removeClass('selected');
	});

	// give breath to the whitelist
	$('#jewelAppBlockSettings > li:last').bind('click', function() {
		if ($('#appBlockWhitelist').is(':hidden')) {
			Menu.updateWhitelist();
			$('#appBlockWhitelist').slideDown('fast');
		} else {
			$('#appBlockWhitelist').hide();
		}
	});

	$filterOption.bind('click', function() {
		Storage.set('blockAppEntries', (Storage.get('blockAppEntries') == 'true' ? 'false' : 'true'));
		Menu.update();
		manipulateFeed(getFeed());
	});

	$highlightOption.bind('click', function() {
		Storage.set('highlightItems', (Storage.get('highlightItems') == 'true' ? 'false' : 'true'));
		Menu.update();
		manipulateFeed(getFeed());
	});

	$unblockPostsWithCommentsOption.bind('click', function() {
		Storage.set('unblockPostsWithComments', (Storage.get('unblockPostsWithComments') == 'true' ? 'false' : 'true'));
		Menu.update();
		manipulateFeed(getFeed());
	});

	$foundBoxOption.bind('click', function() {
		Storage.set('showFoundBox', (Storage.get('showFoundBox') == 'true' ? 'false' : 'true'));
		Menu.update();
		manipulateFeed(getFeed());
	});

	$whitelistOption.children(':first').children(':first').bind('click', function() {
		if (confirm(chrome.i18n.getMessage("menu_warning_reset_whitelist"))) { 
			// reset whitelist			
			Whitelist.reset();
			whitelist = new Whitelist();
			whitelist.load();
			// update menu and feed
			Menu.updateWhitelist();
			manipulateFeed(getFeed());
		}
	});
}

Menu.update = function() {
	for (var key in Menu.index) {
		var menuEntry = Menu.getMenuEntry(key);
		if (key == 'filter') {
			$el = menuEntry.children(':first').children(':first');
			$el.attr('checked', (Storage.get('blockAppEntries') == 'true' ? true : false));
		} else if (key == 'highlight') {
			$el = menuEntry.children(':first').children(':first');
			$el.attr('checked', (Storage.get('highlightItems') == 'true' ? true : false));
		} else if (key == 'foundBox') {
			$el = menuEntry.children(':first').children(':first');
			$el.attr('checked', (Storage.get('showFoundBox') == 'true' ? true : false));
		} else if (key == 'unblockPostsWithComments') {
			$el = menuEntry.children(':first').children(':first');
			$el.attr('checked', (Storage.get('unblockPostsWithComments') == 'true' ? true : false));
		}
	}
}

Menu.getMenuEntry = function(menuName) {
	var id = Menu.index[menuName];
	if (id == null) return null;
	return $('#jewelAppBlockSettings > li').eq(id);
}

Menu.updateWhitelist = function() {
	var $el = $('#appBlockWhitelist').children(':first');
	$el.html(""); // remove old li-entries from previor
	var whitelistedApps = whitelist.getWhitelist();
	var cApp = null;
	while((cApp = whitelistedApps.pop()) != null) {
		var appId = cApp[0];
		var appName = cApp[1];
		var $item = $('<li><a href="/apps/application.php?id=' + appId +'">' + appName + '</a><span><a href="#" appid="'+appId+'" appname="'+appName+'">'+chrome.i18n.getMessage('remove')+'</a></span><li>').appendTo($el);
		// set click-listener on remove-link
		$item.children().eq(1).children(':first').bind('click', function() {
			
			toggleWhitelist($(this).attr('appid'), $(this).attr('appname'));
			manipulateFeed(getFeed());
		});
	}
}


// create the application found box
var $globalContainer = $('#globalContainer');
var $appBlockFoundBox = $('<div id="appBlockFoundBox"><h3>'+chrome.i18n.getMessage('foundbox_headline')+' <a href="#" title="'+chrome.i18n.getMessage('foundbox_close_hint')+'">[x]</a></h3><ul id="appBlockFoundBoxList"></ul></div>');
$appBlockFoundBox.appendTo($globalContainer);
$appBlockFoundBox.children(':first').children(':first').bind('click', function() {
	Storage.set('showFoundBox', 'false');
	Menu.update();
	manipulateFeed(getFeed());
});


function getFeed() {
	var $feed = $("#profile_minifeed>div"); // old profile feed
	if ($feed.length == 0) {
		$feed = $("#home_stream li.uiStreamStory"); // home feed
	}
	if ($feed.length == 0) {
		$feed = $("#profile_minifeed li.uiStreamStory"); // new profile feed
	}
	if ($feed.length == 0) return null;
	return $feed;
}

function activateDomListener() {
	$('#content').bind('DOMNodeInserted', function(el) {  // DOMSubtreeModified
		// we need to work with a timer, otherwise we'll slow down the page noticeable
		clearTimeout(timer);
		timer = setTimeout("manipulateFeed(getFeed())", 300);
	});
}

function deactivateDomListener() {
	$('#content').unbind('DOMNodeInserted'); // DOMSubtreeModified
}

function manipulateFeed($feed) {
	//console.log("manipulateFeed()-call");
	deactivateDomListener(); // important to avoid infinite loops
	resetFoundList();

	// The social network currently uses two different ways to address applications.
	// The one way is to use the URL 
	//    facebook.com/apps/application.php?id=$id1 
	// and the other way is to use the URL
	//    apps.facebook.com/$id2
	// where $id1 is a (large) number and $id2 is text-based.
	//
	// Because we need to find both URLs, we have two regular expressions.
	var re = new RegExp('<a href="(https?://www.facebook.com)?/apps/application.php\\?id=([0-9]+)((?!(hovercard|rel)).)*?>(.*?)</a>');
	var re2 = new RegExp('<span.*?UIActionLinks_bottom.*?<a href="https?://apps.facebook.com/(.*?)/.*?">(.*?)</a></span>');
	var registeredApps = Array();
	var appCounter = 0;
	if ($feed != null) {
		$feed.each(function(idx, el) {
			var m = re.exec($(el).html());
			var m2 = re2.exec($(el).html());
			if (m != null || m2 != null) {
				appCounter++;

				// find out the application id and name
				if (m != null) {
					var appId = m[2];
					var appName = m[5];
				} else if (m2 != null) {
					// This is a very unusual way to get the applications id. Because of the format the social network uses for the http://apps.facebook.com-Links
					// we need to get the id via the applications logo.
					// Due to the restrictions for cross site scripting, it is not possible to get the id via an ajax call to apps.facebook.com.
					// If there is any better way to do this, we should replace this workaround!

					// ----------------------
					// HISTORY
					// ----------------------

					// Before 11 March 2011
					// <img class="img" src="http://photos-c.ak.fbcdn.net/photos-ak-snc1/v43/120/129975336408/app_2_129975336408_7789.gif" alt=""> // test string
					// var imageRegExp = new RegExp('<img.*?src="https?://photos-[a-z]*.ak.fbcdn.net/.*?/([0-9]{5,})/.*?".*?>'); 
					
					// ----------------------
					// END HISTORY
					// ----------------------

					// This is a possible URL we use to get the id.
					//<a class="external UIImageBlock_Image UIImageBlock_MED_Image" href="https://apps.facebook.com/bowlingbuddies/?type=discovery&amp;ref=nf" title="" data-ft="{&quot;type&quot;:&quot;media&quot;}"><img class="img" src="https://fbcdn-photos-a.akamaihd.net/photos-ak-snc1/v43/210/8519508606/app_1_8519508606_84.gif"></a> // test-string
					var imageRegExp = new RegExp('<a.*?href="https?://apps.facebook.com/[a-z]*/.*?><img.*?src="https?://.*?/([0-9]{5,})/');

					var imageRegExpMatches = imageRegExp.exec($(el).html());
					if (imageRegExpMatches == null) { // Abort! This should not happen!
						console.error("An Error has occured while parsing the feed. The feed entry was skipped.");
						return; 
					}
					var appId = imageRegExpMatches[1];
					var appName = m2[2];
				}
				

				// is the application whitelisted yet?
				var whitelisted = whitelist.isWhitelisted(appId);

				// if this application entry is not known yet - set some HTML-code
				if ($(el).attr('appBlock') == null) {
					var $newItem = $('<div class="appBlockPosting"><a href="#" appid="'+appId+'" appname="'+appName+'"></a></div>');
					$newItem.children().first().bind('click', function() {
						toggleWhitelist($(this).attr('appid'), $(this).attr('appname'));
						manipulateFeed(getFeed());
					});
					$(this).append($newItem);
					$(el).attr({'appBlock' : '1'});
				}
				// set some option for the user to decide
				$(el).children(":last").children(":first").text(whitelisted ? chrome.i18n.getMessage('filtering_remove_from_whitelist') : chrome.i18n.getMessage('filtering_add_to_whitelist'));

				// does this post have comments?
				var hasComments = false;
				if (Storage.get('unblockPostsWithComments') == 'true') { // only check if neccessary
					// Therefore we get the unique post ID and check the length of the comment list
					var cId = $(el).attr('id');
					if ($("#"+cId+" ul.commentList").children().size() > 0) {
						hasComments = true;
					}
				}				

				// if this application is seen the first time -> add it to the foundListBox
				if (registeredApps.indexOf(appId) == -1) {
					var $newFoundBoxListItem = $('<li appid="'+appId+'" appname="'+appName+'"><a href="/apps/application.php?id='+appId+'">'+appName+'</a><span class="appaction"><a href="#" appid="'+appId+'" appname="'+appName+'" title="'+chrome.i18n.getMessage('foundbox_toggle_policy_description')+'"></a></span></li>');
					$newFoundBoxListItem.children().eq(1).children(':first').bind('click', function() {
						toggleWhitelist($(this).attr('appid'), $(this).attr('appname'));
						manipulateFeed(getFeed());
					});
					$('#appBlockFoundBoxList').append($newFoundBoxListItem);
					registeredApps.push(appId);
				}

				// hide the annoying postings from the user
				if (!whitelisted && Storage.get('blockAppEntries') == 'true') {
					$(el).css({'display' : (hasComments ? 'block' : 'none')}); // the hasComments-flag is only set to true if the user wants to show app-postings with comments
				} else {
					$(el).css({'display' : 'block'});
				}

				// highlight the application postings?
				if (Storage.get('highlightItems') == 'true') {
					$(el).css({'background-color' : Storage.get('highlightColor')});
				} else {
					$(el).css({'background-color' : 'inherit'});
				}
			}
		});
	}
	// we don't need the found box if there are no app-postings
	if (registeredApps.length == 0 || Storage.get('showFoundBox') == 'false') {
		// hide foundbox
		$('#appBlockFoundBox').css({'display' : 'none'});
	} else {
		// show found box
		$('#appBlockFoundBox').css({'display' : 'block'});
		
		// set some option for the user to decide (in found box)
		$('#appBlockFoundBoxList>li').each(function() {
			$(this).children(':last').children(':first').text(whitelist.isWhitelisted($(this).attr('appid')) ? chrome.i18n.getMessage('filtering_allowed') : chrome.i18n.getMessage('filtering_blocked'));
		});
	}

	// inform the user how many application postings were detected on this page
	$('#appBlockCounter').text(appCounter + ' ' + chrome.i18n.getMessage("entries") + ' (via ' + registeredApps.length + ' ' + chrome.i18n.getMessage("apps")+')');
	if (appCounter > 0) {
		$('#appBlockWrapper span.jewelCount').css('display', 'block');
		$('#appBlockWrapper span.jewelCount>span').html(appCounter);
	} else {
		$('#appBlockWrapper span.jewelCount').css('display', 'none');
	}

	activateDomListener(); // we want to be informed about changes to the DOM again
}

function toggleWhitelist(appId, appName) {
	if (whitelist.isWhitelisted(appId)) {
		whitelist.remove(appId);
	} else {
		whitelist.add(appId, appName);
	}
}

function resetFoundList() {
	$('#appBlockFoundBoxList').empty();
}

/* create timer we'll need later on */
var timer = null;

/* create default settings on running the extension the first time */
Storage.createDefaultValues();

/* create menu for our application */
var menu = new Menu();
Menu.create();
Menu.update();

/* create whitelist and load the user settings */
var whitelist = new Whitelist();
whitelist.load();

// we're ready now to block the annoying postings. come on, let's do this!
activateDomListener();
